diff --git a/app/assets/javascripts/content_editor/extensions/sourcemap.js b/app/assets/javascripts/content_editor/extensions/sourcemap.js
index d8626289f2c6935c1db566e7a6748fc7d0a7f5db..54d69d83188dbb3ff68471344766cd636ab2d8a6 100644
--- a/app/assets/javascripts/content_editor/extensions/sourcemap.js
+++ b/app/assets/javascripts/content_editor/extensions/sourcemap.js
@@ -27,6 +27,7 @@ import Table from './table';
 import TableCell from './table_cell';
 import TableHeader from './table_header';
 import TableRow from './table_row';
+import TableOfContents from './table_of_contents';
 import Video from './video';
 
 export default Extension.create({
@@ -61,6 +62,7 @@ export default Extension.create({
           TableCell.name,
           TableHeader.name,
           TableRow.name,
+          TableOfContents.name,
           Video.name,
           ...HTMLNodes.map((htmlNode) => htmlNode.name),
         ],
diff --git a/app/assets/javascripts/content_editor/services/markdown_serializer.js b/app/assets/javascripts/content_editor/services/markdown_serializer.js
index 5fc7204212b116a977a61400d3207bb5ef71488e..ba0cad6c91c426dbc403d39734773a20e9d1d7a3 100644
--- a/app/assets/javascripts/content_editor/services/markdown_serializer.js
+++ b/app/assets/javascripts/content_editor/services/markdown_serializer.js
@@ -206,10 +206,10 @@ const defaultSerializerConfig = {
       },
       overwriteSourcePreservationStrategy: true,
     }),
-    [TableOfContents.name]: (state, node) => {
+    [TableOfContents.name]: preserveUnchanged((state, node) => {
       state.write('[[_TOC_]]');
       state.closeBlock(node);
-    },
+    }),
     [Table.name]: preserveUnchanged(renderTable),
     [TableCell.name]: renderTableCell,
     [TableHeader.name]: renderTableCell,
diff --git a/app/assets/javascripts/content_editor/services/remark_markdown_deserializer.js b/app/assets/javascripts/content_editor/services/remark_markdown_deserializer.js
index 9113ad5997e9611528dd380f8312bac59fac05d4..ca290efca11df56b87e52e0e39c0442890abcfa6 100644
--- a/app/assets/javascripts/content_editor/services/remark_markdown_deserializer.js
+++ b/app/assets/javascripts/content_editor/services/remark_markdown_deserializer.js
@@ -237,6 +237,11 @@ const factorySpecs = {
       language: hastNode.properties.language,
     }),
   },
+
+  tableOfContents: {
+    type: 'block',
+    selector: 'tableofcontents',
+  },
 };
 
 const SANITIZE_ALLOWLIST = ['level', 'identifier', 'numeric', 'language', 'url', 'isReference'];
@@ -294,6 +299,7 @@ export default () => {
           'yaml',
           'toml',
           'json',
+          'tableOfContents',
         ],
       });
 
diff --git a/spec/frontend/content_editor/remark_markdown_processing_spec.js b/spec/frontend/content_editor/remark_markdown_processing_spec.js
index 61a6fd0debe7107a0116fa35da37b5871cd3f251..bc43af9bd8b515ca3fee980fb61bc135680ce8dc 100644
--- a/spec/frontend/content_editor/remark_markdown_processing_spec.js
+++ b/spec/frontend/content_editor/remark_markdown_processing_spec.js
@@ -23,6 +23,7 @@ import Sourcemap from '~/content_editor/extensions/sourcemap';
 import Strike from '~/content_editor/extensions/strike';
 import Table from '~/content_editor/extensions/table';
 import TableHeader from '~/content_editor/extensions/table_header';
+import TableOfContents from '~/content_editor/extensions/table_of_contents';
 import TableRow from '~/content_editor/extensions/table_row';
 import TableCell from '~/content_editor/extensions/table_cell';
 import TaskList from '~/content_editor/extensions/task_list';
@@ -61,6 +62,7 @@ const tiptapEditor = createTestEditor({
     TableRow,
     TableHeader,
     TableCell,
+    TableOfContents,
     TaskList,
     TaskItem,
     Video,
@@ -98,6 +100,7 @@ const {
     tableRow,
     tableHeader,
     tableCell,
+    tableOfContents,
     taskItem,
     taskList,
     video,
@@ -130,6 +133,7 @@ const {
     tableCell: { nodeType: TableCell.name },
     tableHeader: { nodeType: TableHeader.name },
     tableRow: { nodeType: TableRow.name },
+    tableOfContents: { nodeType: TableOfContents.name },
     taskItem: { nodeType: TaskItem.name },
     taskList: { nodeType: TaskList.name },
     video: { nodeType: Video.name },
@@ -1294,6 +1298,14 @@ content
         expectedDoc: doc(diagram({ ...source(markdown), language }, 'content')),
       };
     }),
+    {
+      markdown: '[[_TOC_]]',
+      expectedDoc: doc(tableOfContents(source('[[_TOC_]]'))),
+    },
+    {
+      markdown: '[TOC]',
+      expectedDoc: doc(tableOfContents(source('[TOC]'))),
+    },
   ];
 
   const runOnly = examples.find((example) => example.only === true);
diff --git a/spec/frontend/content_editor/render_html_and_json_for_all_examples.js b/spec/frontend/content_editor/render_html_and_json_for_all_examples.js
index fc88c0ea44540af535d7ca768e8b051ae4693e6e..bd48b7fdd23236a66884f9cdf5d5ca11aa556ff3 100644
--- a/spec/frontend/content_editor/render_html_and_json_for_all_examples.js
+++ b/spec/frontend/content_editor/render_html_and_json_for_all_examples.js
@@ -34,6 +34,7 @@ import Table from '~/content_editor/extensions/table';
 import TableCell from '~/content_editor/extensions/table_cell';
 import TableHeader from '~/content_editor/extensions/table_header';
 import TableRow from '~/content_editor/extensions/table_row';
+import TableOfContents from '~/content_editor/extensions/table_of_contents';
 import TaskItem from '~/content_editor/extensions/task_item';
 import TaskList from '~/content_editor/extensions/task_list';
 import Video from '~/content_editor/extensions/video';
@@ -75,6 +76,7 @@ const tiptapEditor = createTestEditor({
     TableCell,
     TableHeader,
     TableRow,
+    TableOfContents,
     TaskItem,
     TaskList,
     Video,