diff --git a/app/assets/javascripts/content_editor/components/wrappers/table_cell_base.vue b/app/assets/javascripts/content_editor/components/wrappers/table_cell_base.vue
index 5624bae34c270cd2cfdda3f5db15ef5378b2d3d4..5abacf44cf3f9829f9e41edc14ac91280dce7ffd 100644
--- a/app/assets/javascripts/content_editor/components/wrappers/table_cell_base.vue
+++ b/app/assets/javascripts/content_editor/components/wrappers/table_cell_base.vue
@@ -1,22 +1,71 @@
 <script>
-import { GlDropdown, GlDropdownItem, GlDropdownDivider } from '@gitlab/ui';
+import { GlDisclosureDropdown } from '@gitlab/ui';
 import { NodeViewWrapper, NodeViewContent } from '@tiptap/vue-2';
 import { selectedRect as getSelectedRect } from '@tiptap/pm/tables';
-import { __ } from '~/locale';
+import { __, n__ } from '~/locale';
 
 const TABLE_CELL_HEADER = 'th';
 const TABLE_CELL_BODY = 'td';
 
+function getDropdownItems({ selectedRect, cellType, rowspan = 1, colspan = 1 }) {
+  const totalRows = selectedRect?.map.height;
+  const totalCols = selectedRect?.map.width;
+  const isTableBodyCell = cellType === TABLE_CELL_BODY;
+  const selectedRows = selectedRect ? selectedRect.bottom - selectedRect.top : 0;
+  const selectedCols = selectedRect ? selectedRect.right - selectedRect.left : 0;
+  const showSplitCellOption =
+    selectedRows === rowspan && selectedCols === colspan && (rowspan > 1 || colspan > 1);
+  const showMergeCellsOption = selectedRows !== rowspan || selectedCols !== colspan;
+  const numCellsToMerge = (selectedRows - rowspan + 1) * (selectedCols - colspan + 1);
+  const showDeleteRowOption = totalRows > selectedRows + 1 && isTableBodyCell;
+  const showDeleteColumnOption = totalCols > selectedCols;
+
+  return [
+    {
+      items: [
+        { text: __('Insert column before'), value: 'addColumnBefore' },
+        { text: __('Insert column after'), value: 'addColumnAfter' },
+        isTableBodyCell && { text: __('Insert row before'), value: 'addRowBefore' },
+        { text: __('Insert row after'), value: 'addRowAfter' },
+      ].filter(Boolean),
+    },
+    {
+      items: [
+        showSplitCellOption && { text: __('Split cell'), value: 'splitCell' },
+        showMergeCellsOption && {
+          text: n__('Merge %d cell', 'Merge %d cells', numCellsToMerge),
+          value: 'mergeCells',
+        },
+      ].filter(Boolean),
+    },
+    {
+      items: [
+        showDeleteRowOption && {
+          text: n__('Delete row', 'Delete %d rows', selectedRows),
+          value: 'deleteRow',
+        },
+        showDeleteColumnOption && {
+          text: n__('Delete column', 'Delete %d columns', selectedCols),
+          value: 'deleteColumn',
+        },
+        { text: __('Delete table'), value: 'deleteTable' },
+      ].filter(Boolean),
+    },
+  ].filter(({ items }) => items.length);
+}
+
 export default {
   name: 'TableCellBaseWrapper',
   components: {
     NodeViewWrapper,
     NodeViewContent,
-    GlDropdown,
-    GlDropdownItem,
-    GlDropdownDivider,
+    GlDisclosureDropdown,
   },
   props: {
+    getPos: {
+      type: Function,
+      required: true,
+    },
     cellType: {
       type: String,
       validator: (type) => [TABLE_CELL_HEADER, TABLE_CELL_BODY].includes(type),
@@ -34,19 +83,17 @@ export default {
   data() {
     return {
       displayActionsDropdown: false,
-      preventHide: true,
       selectedRect: null,
     };
   },
   computed: {
-    totalRows() {
-      return this.selectedRect?.map.height;
-    },
-    totalCols() {
-      return this.selectedRect?.map.width;
-    },
-    isTableBodyCell() {
-      return this.cellType === TABLE_CELL_BODY;
+    dropdownItems() {
+      return getDropdownItems({
+        selectedRect: this.selectedRect,
+        cellType: this.cellType,
+        rowspan: this.node.attrs.rowspan,
+        colspan: this.node.attrs.colspan,
+      });
     },
   },
   mounted() {
@@ -61,6 +108,8 @@ export default {
       const { state } = this.editor;
       const { $cursor } = state.selection;
 
+      this.selectedRect = getSelectedRect(state);
+
       if (!$cursor) return;
 
       this.displayActionsDropdown = false;
@@ -71,54 +120,34 @@ export default {
           break;
         }
       }
-
-      if (this.displayActionsDropdown) {
-        this.selectedRect = getSelectedRect(state);
-      }
     },
-    runCommand(command) {
-      this.editor.chain()[command]().run();
+
+    runCommand({ value: command }) {
       this.hideDropdown();
+      this.editor.chain()[command]().run();
     },
-    handleHide($event) {
-      if (this.preventHide) {
-        $event.preventDefault();
-      }
-      this.preventHide = true;
-    },
+
     hideDropdown() {
-      this.preventHide = false;
-      this.$refs.dropdown?.hide();
+      this.$refs.dropdown?.close();
     },
   },
-  i18n: {
-    insertColumnBefore: __('Insert column before'),
-    insertColumnAfter: __('Insert column after'),
-    insertRowBefore: __('Insert row before'),
-    insertRowAfter: __('Insert row after'),
-    deleteRow: __('Delete row'),
-    deleteColumn: __('Delete column'),
-    deleteTable: __('Delete table'),
-    editTableActions: __('Edit table'),
-  },
-  dropdownPopperOpts: {
-    positionFixed: true,
-  },
 };
 </script>
 <template>
   <node-view-wrapper
-    class="gl-relative gl-padding-5 gl-min-w-10"
     :as="cellType"
+    :rowspan="node.attrs.rowspan || 1"
+    :colspan="node.attrs.colspan || 1"
     dir="auto"
+    class="gl-m-0! gl-p-0! gl-relative"
     @click="hideDropdown"
   >
     <span
       v-if="displayActionsDropdown"
       contenteditable="false"
-      class="gl-absolute gl-right-0 gl-top-0"
+      class="gl-absolute gl-right-0 gl-top-0 gl-pr-1 gl-pt-1"
     >
-      <gl-dropdown
+      <gl-disclosure-dropdown
         ref="dropdown"
         dropup
         icon="chevron-down"
@@ -127,34 +156,12 @@ export default {
         boundary="viewport"
         no-caret
         text-sr-only
-        :text="$options.i18n.editTableActions"
-        :popper-opts="$options.dropdownPopperOpts"
-        @hide="handleHide($event)"
-      >
-        <gl-dropdown-item @click="runCommand('addColumnBefore')">
-          {{ $options.i18n.insertColumnBefore }}
-        </gl-dropdown-item>
-        <gl-dropdown-item @click="runCommand('addColumnAfter')">
-          {{ $options.i18n.insertColumnAfter }}
-        </gl-dropdown-item>
-        <gl-dropdown-item v-if="isTableBodyCell" @click="runCommand('addRowBefore')">
-          {{ $options.i18n.insertRowBefore }}
-        </gl-dropdown-item>
-        <gl-dropdown-item @click="runCommand('addRowAfter')">
-          {{ $options.i18n.insertRowAfter }}
-        </gl-dropdown-item>
-        <gl-dropdown-divider />
-        <gl-dropdown-item v-if="totalRows > 2 && isTableBodyCell" @click="runCommand('deleteRow')">
-          {{ $options.i18n.deleteRow }}
-        </gl-dropdown-item>
-        <gl-dropdown-item v-if="totalCols > 1" @click="runCommand('deleteColumn')">
-          {{ $options.i18n.deleteColumn }}
-        </gl-dropdown-item>
-        <gl-dropdown-item @click="runCommand('deleteTable')">
-          {{ $options.i18n.deleteTable }}
-        </gl-dropdown-item>
-      </gl-dropdown>
+        :items="dropdownItems"
+        :toggle-text="__('Edit table')"
+        positioning-strategy="fixed"
+        @action="runCommand"
+      />
     </span>
-    <node-view-content />
+    <node-view-content as="div" class="gl-p-5 gl-min-w-10" />
   </node-view-wrapper>
 </template>
diff --git a/app/assets/stylesheets/components/content_editor.scss b/app/assets/stylesheets/components/content_editor.scss
index 7f66d335f41a0e3f8d84777fe4f2fe826410727d..680b24be442f7d2cf39374642a4d0cf71f685b76 100644
--- a/app/assets/stylesheets/components/content_editor.scss
+++ b/app/assets/stylesheets/components/content_editor.scss
@@ -33,6 +33,17 @@
     outline-offset: -3px;
   }
 
+  .selectedCell::after {
+    content: '';
+    position: absolute;
+    top: 0;
+    left: 0;
+    right: 0;
+    bottom: 0;
+    background-color: rgba($blue-200, 0.25);
+    pointer-events: none;
+  }
+
   video {
     max-width: 400px;
   }
@@ -115,6 +126,15 @@
       display: inherit;
     }
   }
+
+  .gl-new-dropdown-inner li {
+    margin-left: 0 !important;
+
+    &.gl-new-dropdown-item {
+      padding-left: $gl-spacing-scale-2;
+      padding-right: $gl-spacing-scale-2;
+    }
+  }
 }
 
 .table-creator-grid-item {
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index d71636d3580e79fbe9e117050c60cd730b40e03c..313e37cb81a91bc14ac53e861c997afc3d3a10d0 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -14742,7 +14742,9 @@ msgid "Delete code block"
 msgstr ""
 
 msgid "Delete column"
-msgstr ""
+msgid_plural "Delete %d columns"
+msgstr[0] ""
+msgstr[1] ""
 
 msgid "Delete comment"
 msgstr ""
@@ -14802,7 +14804,9 @@ msgid "Delete release %{release}?"
 msgstr ""
 
 msgid "Delete row"
-msgstr ""
+msgid_plural "Delete %d rows"
+msgstr[0] ""
+msgstr[1] ""
 
 msgid "Delete selected"
 msgstr ""
@@ -28282,6 +28286,11 @@ msgstr ""
 msgid "Merge"
 msgstr ""
 
+msgid "Merge %d cell"
+msgid_plural "Merge %d cells"
+msgstr[0] ""
+msgstr[1] ""
+
 msgid "Merge Conflicts"
 msgstr ""
 
@@ -43667,6 +43676,9 @@ msgstr ""
 msgid "Spent at can't be a future date and time."
 msgstr ""
 
+msgid "Split cell"
+msgstr ""
+
 msgid "Squash commit message"
 msgstr ""
 
diff --git a/spec/frontend/content_editor/components/wrappers/table_cell_base_spec.js b/spec/frontend/content_editor/components/wrappers/table_cell_base_spec.js
index 0d56280d63039f542419431f01172a0e3f76df64..275f48ea857c2ed0d1d64abe51f41ea0a0f18efd 100644
--- a/spec/frontend/content_editor/components/wrappers/table_cell_base_spec.js
+++ b/spec/frontend/content_editor/components/wrappers/table_cell_base_spec.js
@@ -1,8 +1,8 @@
-import { GlDropdown, GlDropdownItem } from '@gitlab/ui';
-import { NodeViewWrapper } from '@tiptap/vue-2';
+import { GlDisclosureDropdown } from '@gitlab/ui';
+import { NodeViewWrapper, NodeViewContent } from '@tiptap/vue-2';
 import { selectedRect as getSelectedRect } from '@tiptap/pm/tables';
 import { nextTick } from 'vue';
-import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import { mountExtended } from 'helpers/vue_test_utils_helper';
 import { stubComponent } from 'helpers/stub_component';
 import TableCellBaseWrapper from '~/content_editor/components/wrappers/table_cell_base.vue';
 import { createTestEditor, mockChainedCommands, emitEditorEvent } from '../../test_utils';
@@ -15,32 +15,21 @@ describe('content/components/wrappers/table_cell_base', () => {
   let node;
 
   const createWrapper = (propsData = { cellType: 'td' }) => {
-    wrapper = shallowMountExtended(TableCellBaseWrapper, {
+    wrapper = mountExtended(TableCellBaseWrapper, {
       propsData: {
         editor,
         node,
+        getPos: () => 0,
         ...propsData,
       },
       stubs: {
-        GlDropdown: stubComponent(GlDropdown, {
-          methods: {
-            hide: jest.fn(),
-          },
-        }),
+        NodeViewWrapper: stubComponent(NodeViewWrapper),
+        NodeViewContent: stubComponent(NodeViewContent),
       },
     });
   };
 
-  const findDropdown = () => wrapper.findComponent(GlDropdown);
-  const findDropdownItemWithLabel = (name) =>
-    wrapper
-      .findAllComponents(GlDropdownItem)
-      .filter((dropdownItem) => dropdownItem.text().includes(name))
-      .at(0);
-  const findDropdownItemWithLabelExists = (name) =>
-    wrapper
-      .findAllComponents(GlDropdownItem)
-      .filter((dropdownItem) => dropdownItem.text().includes(name)).length > 0;
+  const findDropdown = () => wrapper.findComponent(GlDisclosureDropdown);
   const setCurrentPositionInCell = () => {
     const { $cursor } = editor.state.selection;
 
@@ -48,7 +37,9 @@ describe('content/components/wrappers/table_cell_base', () => {
   };
 
   beforeEach(() => {
-    node = {};
+    node = {
+      attrs: {},
+    };
     editor = createTestEditor({});
   });
 
@@ -68,11 +59,10 @@ describe('content/components/wrappers/table_cell_base', () => {
       category: 'tertiary',
       icon: 'chevron-down',
       size: 'small',
-      split: false,
+      noCaret: true,
     });
     expect(findDropdown().attributes()).toMatchObject({
       boundary: 'viewport',
-      'no-caret': '',
     });
   });
 
@@ -88,6 +78,10 @@ describe('content/components/wrappers/table_cell_base', () => {
     beforeEach(async () => {
       setCurrentPositionInCell();
       getSelectedRect.mockReturnValue({
+        top: 0,
+        left: 0,
+        bottom: 1,
+        right: 1,
         map: {
           height: 1,
           width: 1,
@@ -107,81 +101,176 @@ describe('content/components/wrappers/table_cell_base', () => {
       ${'Delete table'}         | ${'deleteTable'}
     `(
       'executes $commandName when $dropdownItemLabel button is clicked',
-      ({ commandName, dropdownItemLabel }) => {
+      async ({ dropdownItemLabel, commandName }) => {
         const mocks = mockChainedCommands(editor, [commandName, 'run']);
 
-        findDropdownItemWithLabel(dropdownItemLabel).vm.$emit('click');
+        await wrapper.findByRole('button', { name: dropdownItemLabel }).trigger('click');
 
         expect(mocks[commandName]).toHaveBeenCalled();
       },
     );
 
-    it('does not allow deleting rows and columns', () => {
-      expect(findDropdownItemWithLabelExists('Delete row')).toBe(false);
-      expect(findDropdownItemWithLabelExists('Delete column')).toBe(false);
+    it.each`
+      dropdownItemLabel
+      ${'Delete row'}
+      ${'Delete column'}
+      ${'Split cell'}
+      ${'Merge'}
+    `('does not have option $dropdownItemLabel available', ({ dropdownItemLabel }) => {
+      expect(findDropdown().text()).not.toContain(dropdownItemLabel);
     });
 
-    it('allows deleting rows when there are more than 2 rows in the table', async () => {
-      const mocks = mockChainedCommands(editor, ['deleteRow', 'run']);
+    it.each`
+      dropdownItemLabel  | commandName
+      ${'Delete row'}    | ${'deleteRow'}
+      ${'Delete column'} | ${'deleteColumn'}
+    `(
+      'allows $dropdownItemLabel operation when there are more than 2 rows and 1 column in the table',
+      async ({ dropdownItemLabel, commandName }) => {
+        const mocks = mockChainedCommands(editor, [commandName, 'run']);
 
-      getSelectedRect.mockReturnValue({
-        map: {
-          height: 3,
-        },
-      });
+        getSelectedRect.mockReturnValue({
+          top: 0,
+          left: 0,
+          bottom: 1,
+          right: 1,
+          map: {
+            height: 3,
+            width: 2,
+          },
+        });
 
-      emitEditorEvent({ tiptapEditor: editor, event: 'selectionUpdate' });
+        emitEditorEvent({ tiptapEditor: editor, event: 'selectionUpdate' });
 
-      await nextTick();
+        await nextTick();
+        await wrapper.findByRole('button', { name: dropdownItemLabel }).trigger('click');
 
-      findDropdownItemWithLabel('Delete row').vm.$emit('click');
+        expect(mocks[commandName]).toHaveBeenCalled();
+      },
+    );
 
-      expect(mocks.deleteRow).toHaveBeenCalled();
-    });
+    describe("when current row is the table's header", () => {
+      beforeEach(async () => {
+        // Remove 2 rows condition
+        getSelectedRect.mockReturnValue({
+          map: {
+            height: 3,
+          },
+        });
 
-    it('allows deleting columns when there are more than 1 column in the table', async () => {
-      const mocks = mockChainedCommands(editor, ['deleteColumn', 'run']);
+        createWrapper({ cellType: 'th' });
 
-      getSelectedRect.mockReturnValue({
-        map: {
-          width: 2,
-        },
+        await nextTick();
       });
 
-      emitEditorEvent({ tiptapEditor: editor, event: 'selectionUpdate' });
+      it('does not allow adding a row before the header', () => {
+        expect(findDropdown().text()).not.toContain('Insert row before');
+      });
 
-      await nextTick();
+      it('does not allow removing the header row', async () => {
+        createWrapper({ cellType: 'th' });
 
-      findDropdownItemWithLabel('Delete column').vm.$emit('click');
+        await nextTick();
 
-      expect(mocks.deleteColumn).toHaveBeenCalled();
+        expect(findDropdown().text()).not.toContain('Delete row');
+      });
     });
 
-    describe('when current row is the table’s header', () => {
-      beforeEach(async () => {
-        // Remove 2 rows condition
+    describe.each`
+      attrs             | rect
+      ${{ rowspan: 2 }} | ${{ top: 0, left: 0, bottom: 2, right: 1 }}
+      ${{ colspan: 2 }} | ${{ top: 0, left: 0, bottom: 1, right: 2 }}
+    `('when selected cell has $attrs', ({ attrs, rect }) => {
+      beforeEach(() => {
+        node = { attrs };
+
         getSelectedRect.mockReturnValue({
+          ...rect,
           map: {
             height: 3,
+            width: 2,
           },
         });
 
-        createWrapper({ cellType: 'th' });
+        setCurrentPositionInCell();
+      });
+
+      it('allows splitting the cell', async () => {
+        const mocks = mockChainedCommands(editor, ['splitCell', 'run']);
+
+        createWrapper();
 
         await nextTick();
+        await wrapper.findByRole('button', { name: 'Split cell' }).trigger('click');
+
+        expect(mocks.splitCell).toHaveBeenCalled();
       });
+    });
 
-      it('does not allow adding a row before the header', () => {
-        expect(findDropdownItemWithLabelExists('Insert row before')).toBe(false);
+    describe('when selected cell has rowspan=2 and colspan=2', () => {
+      beforeEach(() => {
+        node = { attrs: { rowspan: 2, colspan: 2 } };
+        const rect = { top: 1, left: 1, bottom: 3, right: 3 };
+
+        getSelectedRect.mockReturnValue({
+          ...rect,
+          map: { height: 5, width: 5 },
+        });
+
+        setCurrentPositionInCell();
       });
 
-      it('does not allow removing the header row', async () => {
-        createWrapper({ cellType: 'th' });
+      it.each`
+        type         | dropdownItemLabel     | commandName
+        ${'rows'}    | ${'Delete 2 rows'}    | ${'deleteRow'}
+        ${'columns'} | ${'Delete 2 columns'} | ${'deleteColumn'}
+      `('shows correct label for deleting $type', async ({ dropdownItemLabel, commandName }) => {
+        const mocks = mockChainedCommands(editor, [commandName, 'run']);
+
+        createWrapper();
 
         await nextTick();
+        await wrapper.findByRole('button', { name: dropdownItemLabel }).trigger('click');
 
-        expect(findDropdownItemWithLabelExists('Delete row')).toBe(false);
+        expect(mocks[commandName]).toHaveBeenCalled();
       });
     });
+
+    describe.each`
+      rows | cols | product
+      ${2} | ${1} | ${2}
+      ${1} | ${2} | ${2}
+      ${2} | ${2} | ${4}
+    `('when $rows x $cols ($product) cells are selected', ({ rows, cols, product }) => {
+      it.each`
+        dropdownItemLabel                                          | commandName
+        ${`Merge ${product} cells`}                                | ${'mergeCells'}
+        ${rows === 1 ? 'Delete row' : `Delete ${rows} rows`}       | ${'deleteRow'}
+        ${cols === 1 ? 'Delete column' : `Delete ${cols} columns`} | ${'deleteColumn'}
+      `(
+        'executes $commandName when $dropdownItemLabel is clicked',
+        async ({ dropdownItemLabel, commandName }) => {
+          const mocks = mockChainedCommands(editor, [commandName, 'run']);
+
+          getSelectedRect.mockReturnValue({
+            top: 0,
+            left: 0,
+            bottom: rows,
+            right: cols,
+            map: {
+              height: 4,
+              width: 4,
+            },
+          });
+
+          emitEditorEvent({ tiptapEditor: editor, event: 'selectionUpdate' });
+
+          await nextTick();
+          await wrapper.findByRole('button', { name: dropdownItemLabel }).trigger('click');
+
+          expect(mocks[commandName]).toHaveBeenCalled();
+        },
+      );
+    });
   });
 });