diff --git a/course/content.py b/course/content.py
index 2a31b47f739c43bd30f5fc7a69f51b060a5b1556..f4b51531bce925436588949d8912ad2da2a626d4 100644
--- a/course/content.py
+++ b/course/content.py
@@ -912,18 +912,6 @@ def expand_markup(
     return text
 
 
-def unwrap_relate_tmp_pre_tag(html_string):
-    # type: (Text) -> (Text)
-
-    from lxml.html import fromstring, tostring
-    tree = fromstring(html_string)
-
-    for node in tree.iterdescendants("pre"):
-        if "relate_tmp_pre" in node.attrib.get("class", ""):
-            node.drop_tag()
-    return tostring(tree, encoding="unicode")
-
-
 def markup_to_html(
         course,  # type: Optional[Course]
         repo,  # type: Repo_ish
@@ -976,11 +964,13 @@ def markup_to_html(
         return ""
 
     from course.mdx_mathjax import MathJaxExtension
+    from course.utils import NBConvertExtension
     import markdown
 
     extensions = [
         LinkFixerExtension(course, commit_sha, reverse_func=reverse_func),
         MathJaxExtension(),
+        NBConvertExtension(),
         "markdown.extensions.extra",
     ]
 
@@ -998,9 +988,6 @@ def markup_to_html(
         extensions=extensions,
         output_format="html5")
 
-    if result.strip():
-        result = unwrap_relate_tmp_pre_tag(result)
-
     assert isinstance(result, six.text_type)
     if cache_key is not None:
         def_cache.add(cache_key, result, None)
diff --git a/course/templates/course/jinja2/nbconvert_template.tpl b/course/templates/course/jinja2/nbconvert_template.tpl
index f040623ae182dcd75792303c0b86b022075da044..c59f9a8b022622284ef1655f461394f643cf84b2 100644
--- a/course/templates/course/jinja2/nbconvert_template.tpl
+++ b/course/templates/course/jinja2/nbconvert_template.tpl
@@ -2,7 +2,7 @@
 
 {# This is to prevent code_cell being process by markdown_to_html #}
 
-{% block input %}<pre class="relate_tmp_pre">{{ super() }}</pre>
+{% block input %}<pre><relate_ipynb>{{ super() }}</relate_ipynb></pre>
 {%- endblock input %}
 
 {# This is to remove the empty cells ahead of markdown_cells #}
diff --git a/course/utils.py b/course/utils.py
index 428576ba190f5e42209833fbff8be484ada4f9d6..b4ca3ea9b019ed6d289600ba47ef106fabeb194b 100644
--- a/course/utils.py
+++ b/course/utils.py
@@ -28,6 +28,7 @@ from typing import cast
 
 import six
 import datetime  # noqa
+import markdown
 
 from django.shortcuts import (  # noqa
         render, get_object_or_404)
@@ -1322,6 +1323,22 @@ class IpynbJinjaMacro(RelateJinjaMacroBase):
 
         return body
 
+
+NBCONVERT_PRE_OPEN_RE = re.compile(r"<pre\s*>\s*<relate_ipynb\s*>")
+NBCONVERT_PRE_CLOSE_RE = re.compile(r"</relate_ipynb\s*>\s*</pre\s*>")
+
+
+class NBConvertHTMLPostprocessor(markdown.postprocessors.Postprocessor):
+    def run(self, text):
+        text = NBCONVERT_PRE_OPEN_RE.sub("", text)
+        text = NBCONVERT_PRE_CLOSE_RE.sub("", text)
+        return text
+
+
+class NBConvertExtension(markdown.Extension):
+    def extendMarkdown(self, md, md_globals):  # noqa
+        md.postprocessors['relate_nbconvert'] = NBConvertHTMLPostprocessor(md)
+
 # }}}
 
 # vim: foldmethod=marker
diff --git a/requirements.txt b/requirements.txt
index 3dc2f8e9ad884e797326ec402c9b7d914898130c..62959600441b1becd52536efeeb14336352aeaff 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -117,7 +117,4 @@ typing
 nbconvert>=5.2.1
 IPython
 
-# For parsing and edit html
-lxml
-
 # vim: foldmethod=marker
diff --git a/setup.py b/setup.py
index 286d45ec972ebd20a69826ab85ff6dc0de969577..64eee057b18732564af3cde21cacaad9c77822d4 100644
--- a/setup.py
+++ b/setup.py
@@ -27,7 +27,6 @@ setup(name="relate-courseware",
           "markdown",
           "dulwich",
           "pyyaml",
-          "lxml",
           "nbconvert>=5.2.1",
           "pymbolic",
           "sympy",
diff --git a/tests/test_content.py b/tests/test_content.py
index 589e9f3c87f3b3b5f296704f53dbe22e62617693..e8f37b04915ac1a979b5880e2e0fa113e810e1c0 100644
--- a/tests/test_content.py
+++ b/tests/test_content.py
@@ -240,6 +240,7 @@ CODE_CELL_HTML_CLASS = "code_cell"
 CODE_CELL_IN_STR_PATTERN = '<div class="prompt input_prompt">In[%s]:</div>'
 CODE_CELL_PRINT_STR1 = "This is function1"
 CODE_CELL_PRINT_STR2 = "This is function2"
+RELATE_IPYNB_CONVERT_PRE_WRAPPER_TAG_NAME = "relate_ipynb"
 
 
 def strip_nbsp(s):
@@ -261,6 +262,7 @@ class NbconvertRenderTestMixin(SingleCoursePageSandboxTestBaseMixin):
         self.assertNotContains(response, "```")
         self.assertNotContains(response, "# First Title of Test NoteBook")
         self.assertNotContains(response, "# Second Title of Test NoteBook")
+        self.assertNotContains(response, RELATE_IPYNB_CONVERT_PRE_WRAPPER_TAG_NAME)
 
     def setUp(self):
         super(NbconvertRenderTestMixin, self).setUp()