diff --git a/resources/js/wysiwyg/todo.md b/resources/js/wysiwyg/todo.md
index dcc866888..5df26bd8c 100644
--- a/resources/js/wysiwyg/todo.md
+++ b/resources/js/wysiwyg/todo.md
@@ -2,7 +2,7 @@
 
 ## In progress
 
-- Table Cut/Copy/Paste column
+//
 
 ## Main Todo
 
diff --git a/resources/js/wysiwyg/ui/defaults/buttons/tables.ts b/resources/js/wysiwyg/ui/defaults/buttons/tables.ts
index 1a9ffb0d3..49e36bdac 100644
--- a/resources/js/wysiwyg/ui/defaults/buttons/tables.ts
+++ b/resources/js/wysiwyg/ui/defaults/buttons/tables.ts
@@ -29,9 +29,15 @@ import {
 } from "../../../utils/tables";
 import {$isCustomTableRowNode} from "../../../nodes/custom-table-row";
 import {
+    $copySelectedColumnsToClipboard,
     $copySelectedRowsToClipboard,
+    $cutSelectedColumnsToClipboard,
     $cutSelectedRowsToClipboard,
-    $pasteClipboardRowsBefore, $pasteRowsAfter, isRowClipboardEmpty
+    $pasteClipboardRowsBefore,
+    $pasteClipboardRowsAfter,
+    isColumnClipboardEmpty,
+    isRowClipboardEmpty,
+    $pasteClipboardColumnsBefore, $pasteClipboardColumnsAfter
 } from "../../../utils/table-copy-paste";
 
 const neverActive = (): boolean => false;
@@ -180,7 +186,7 @@ export const cutRow: EditorButtonDefinition = {
             try {
                 $cutSelectedRowsToClipboard();
             } catch (e: any) {
-                context.error(e.toString());
+                context.error(e);
             }
         });
     },
@@ -196,7 +202,7 @@ export const copyRow: EditorButtonDefinition = {
             try {
                 $copySelectedRowsToClipboard();
             } catch (e: any) {
-                context.error(e.toString());
+                context.error(e);
             }
         });
     },
@@ -212,7 +218,7 @@ export const pasteRowBefore: EditorButtonDefinition = {
             try {
                 $pasteClipboardRowsBefore(context.editor);
             } catch (e: any) {
-                context.error(e.toString());
+                context.error(e);
             }
         });
     },
@@ -226,9 +232,9 @@ export const pasteRowAfter: EditorButtonDefinition = {
     action(context: EditorUiContext) {
         context.editor.update(() => {
             try {
-                $pasteRowsAfter(context.editor);
+                $pasteClipboardRowsAfter(context.editor);
             } catch (e: any) {
-                context.error(e.toString());
+                context.error(e);
             }
         });
     },
@@ -240,8 +246,12 @@ export const cutColumn: EditorButtonDefinition = {
     label: 'Cut column',
     format: 'long',
     action(context: EditorUiContext) {
-        context.editor.getEditorState().read(() => {
-            // TODO
+        context.editor.update(() => {
+            try {
+                $cutSelectedColumnsToClipboard();
+            } catch (e: any) {
+                context.error(e);
+            }
         });
     },
     isActive: neverActive,
@@ -253,7 +263,11 @@ export const copyColumn: EditorButtonDefinition = {
     format: 'long',
     action(context: EditorUiContext) {
         context.editor.getEditorState().read(() => {
-            // TODO
+            try {
+                $copySelectedColumnsToClipboard();
+            } catch (e: any) {
+                context.error(e);
+            }
         });
     },
     isActive: neverActive,
@@ -264,24 +278,32 @@ export const pasteColumnBefore: EditorButtonDefinition = {
     label: 'Paste column before',
     format: 'long',
     action(context: EditorUiContext) {
-        context.editor.getEditorState().read(() => {
-            // TODO
+        context.editor.update(() => {
+            try {
+                $pasteClipboardColumnsBefore(context.editor);
+            } catch (e: any) {
+                context.error(e);
+            }
         });
     },
     isActive: neverActive,
-    isDisabled: cellNotSelected,
+    isDisabled: (selection) => cellNotSelected(selection) || isColumnClipboardEmpty(),
 };
 
 export const pasteColumnAfter: EditorButtonDefinition = {
     label: 'Paste column after',
     format: 'long',
     action(context: EditorUiContext) {
-        context.editor.getEditorState().read(() => {
-            // TODO
+        context.editor.update(() => {
+            try {
+                $pasteClipboardColumnsAfter(context.editor);
+            } catch (e: any) {
+                context.error(e);
+            }
         });
     },
     isActive: neverActive,
-    isDisabled: cellNotSelected,
+    isDisabled: (selection) => cellNotSelected(selection) || isColumnClipboardEmpty(),
 };
 
 export const insertColumnBefore: EditorButtonDefinition = {
diff --git a/resources/js/wysiwyg/ui/framework/core.ts b/resources/js/wysiwyg/ui/framework/core.ts
index a04f3c74a..3433b96e8 100644
--- a/resources/js/wysiwyg/ui/framework/core.ts
+++ b/resources/js/wysiwyg/ui/framework/core.ts
@@ -14,7 +14,7 @@ export type EditorUiContext = {
     containerDOM: HTMLElement; // DOM element which contains all editor elements
     scrollDOM: HTMLElement; // DOM element which is the main content scroll container
     translate: (text: string) => string; // Translate function
-    error: (text: string) => void; // Error reporting function
+    error: (text: string|Error) => void; // Error reporting function
     manager: EditorUIManager; // UI Manager instance for this editor
     options: Record<string, any>; // General user options which may be used by sub elements
 };
diff --git a/resources/js/wysiwyg/ui/index.ts b/resources/js/wysiwyg/ui/index.ts
index bfa76bb82..3b6d195b7 100644
--- a/resources/js/wysiwyg/ui/index.ts
+++ b/resources/js/wysiwyg/ui/index.ts
@@ -21,8 +21,9 @@ export function buildEditorUI(container: HTMLElement, element: HTMLElement, scro
         scrollDOM: scrollContainer,
         manager,
         translate: (text: string): string => text, // TODO - Implement
-        error(error: string): void {
-            window.$events.error(error); // TODO - Translate
+        error(error: string|Error): void {
+            const message = error instanceof Error ? error.message : error;
+            window.$events.error(message); // TODO - Translate
         },
         options,
     };
diff --git a/resources/js/wysiwyg/utils/node-clipboard.ts b/resources/js/wysiwyg/utils/node-clipboard.ts
index 385c4c46c..dd3b4dfbe 100644
--- a/resources/js/wysiwyg/utils/node-clipboard.ts
+++ b/resources/js/wysiwyg/utils/node-clipboard.ts
@@ -30,13 +30,8 @@ function unserializeNodeRecursive(editor: LexicalEditor, {node, children}: Seria
 }
 
 export class NodeClipboard<T extends LexicalNode> {
-    nodeClass: {importJSON: (s: SerializedLexicalNode) => T};
     protected store: SerializedLexicalNodeWithChildren[] = [];
 
-    constructor(nodeClass: {importJSON: (s: any) => T}) {
-        this.nodeClass = nodeClass;
-    }
-
     set(...nodes: LexicalNode[]): void {
         this.store.splice(0, this.store.length);
         for (const node of nodes) {
diff --git a/resources/js/wysiwyg/utils/table-copy-paste.ts b/resources/js/wysiwyg/utils/table-copy-paste.ts
index ae8ef3d35..12c19b0fb 100644
--- a/resources/js/wysiwyg/utils/table-copy-paste.ts
+++ b/resources/js/wysiwyg/utils/table-copy-paste.ts
@@ -1,12 +1,14 @@
 import {NodeClipboard} from "./node-clipboard";
 import {CustomTableRowNode} from "../nodes/custom-table-row";
-import {$getTableFromSelection, $getTableRowsFromSelection} from "./tables";
-import {$getSelection, LexicalEditor} from "lexical";
-import {$createCustomTableCellNode, $isCustomTableCellNode} from "../nodes/custom-table-cell";
+import {$getTableCellsFromSelection, $getTableFromSelection, $getTableRowsFromSelection} from "./tables";
+import {$getSelection, BaseSelection, LexicalEditor} from "lexical";
+import {$createCustomTableCellNode, $isCustomTableCellNode, CustomTableCellNode} from "../nodes/custom-table-cell";
 import {CustomTableNode} from "../nodes/custom-table";
 import {TableMap} from "./table-map";
+import {$isTableSelection} from "@lexical/table";
+import {$getNodeFromSelection} from "./selection";
 
-const rowClipboard: NodeClipboard<CustomTableRowNode> = new NodeClipboard<CustomTableRowNode>(CustomTableRowNode);
+const rowClipboard: NodeClipboard<CustomTableRowNode> = new NodeClipboard<CustomTableRowNode>();
 
 export function isRowClipboardEmpty(): boolean {
     return rowClipboard.size() === 0;
@@ -82,7 +84,7 @@ export function $pasteClipboardRowsBefore(editor: LexicalEditor): void {
     }
 }
 
-export function $pasteRowsAfter(editor: LexicalEditor): void {
+export function $pasteClipboardRowsAfter(editor: LexicalEditor): void {
     const selection = $getSelection();
     const rows = $getTableRowsFromSelection(selection);
     const table = $getTableFromSelection(selection);
@@ -94,4 +96,177 @@ export function $pasteRowsAfter(editor: LexicalEditor): void {
             lastRow.insertAfter(row);
         }
     }
+}
+
+const columnClipboard: NodeClipboard<CustomTableCellNode>[] = [];
+
+function setColumnClipboard(columns: CustomTableCellNode[][]): void {
+    const newClipboards = columns.map(cells => {
+        const clipboard = new NodeClipboard<CustomTableCellNode>();
+        clipboard.set(...cells);
+        return clipboard;
+    });
+
+    columnClipboard.splice(0, columnClipboard.length, ...newClipboards);
+}
+
+type TableRange = {from: number, to: number};
+
+export function isColumnClipboardEmpty(): boolean {
+    return columnClipboard.length === 0;
+}
+
+function $getSelectionColumnRange(selection: BaseSelection|null): TableRange|null {
+    if ($isTableSelection(selection)) {
+        const shape = selection.getShape()
+        return {from: shape.fromX, to: shape.toX};
+    }
+
+    const cell = $getNodeFromSelection(selection, $isCustomTableCellNode);
+    const table = $getTableFromSelection(selection);
+    if (!$isCustomTableCellNode(cell) || !table) {
+        return null;
+    }
+
+    const map = new TableMap(table);
+    const range = map.getRangeForCell(cell);
+    if (!range) {
+        return null;
+    }
+
+    return {from: range.fromX, to: range.toX};
+}
+
+function $getTableColumnCellsFromSelection(range: TableRange, table: CustomTableNode): CustomTableCellNode[][] {
+    const map = new TableMap(table);
+    const columns = [];
+    for (let x = range.from; x <= range.to; x++) {
+        const cells = map.getCellsInColumn(x);
+        columns.push(cells);
+    }
+
+    return columns;
+}
+
+function validateColumnsToCopy(columns: CustomTableCellNode[][]): void {
+    let commonColSize: number|null = null;
+
+    for (const cells of columns) {
+        let colSize = 0;
+        for (const cell of cells) {
+            colSize += cell.getRowSpan() || 1;
+            if (cell.getColSpan() > 1) {
+                throw Error('Cannot copy columns with merged cells');
+            }
+        }
+
+        if (commonColSize === null) {
+            commonColSize = colSize;
+        } else if (commonColSize !== colSize) {
+            throw Error('Cannot copy columns with inconsistent sizes');
+        }
+    }
+}
+
+export function $cutSelectedColumnsToClipboard(): void {
+    const selection = $getSelection();
+    const range = $getSelectionColumnRange(selection);
+    const table = $getTableFromSelection(selection);
+    if (!range || !table) {
+        return;
+    }
+
+    const colWidths = table.getColWidths();
+    const columns = $getTableColumnCellsFromSelection(range, table);
+    validateColumnsToCopy(columns);
+    setColumnClipboard(columns);
+    for (const cells of columns) {
+        for (const cell of cells) {
+            cell.remove();
+        }
+    }
+
+    const newWidths = [...colWidths].splice(range.from, (range.to - range.from) + 1);
+    table.setColWidths(newWidths);
+}
+
+export function $copySelectedColumnsToClipboard(): void {
+    const selection = $getSelection();
+    const range = $getSelectionColumnRange(selection);
+    const table = $getTableFromSelection(selection);
+    if (!range || !table) {
+        return;
+    }
+
+    const columns = $getTableColumnCellsFromSelection(range, table);
+    validateColumnsToCopy(columns);
+    setColumnClipboard(columns);
+}
+
+function validateColumnsToPaste(columns: CustomTableCellNode[][], targetTable: CustomTableNode) {
+    const tableRowCount = (new TableMap(targetTable)).rowCount;
+    for (const cells of columns) {
+        let colSize = 0;
+        for (const cell of cells) {
+            colSize += cell.getRowSpan() || 1;
+        }
+
+        if (colSize > tableRowCount) {
+            throw Error('Cannot paste columns that are taller than target table');
+        }
+
+        while (colSize < tableRowCount) {
+            cells.push($createCustomTableCellNode());
+            colSize++;
+        }
+    }
+}
+
+function $pasteClipboardColumns(editor: LexicalEditor, isBefore: boolean): void {
+    const selection = $getSelection();
+    const table = $getTableFromSelection(selection);
+    const cells = $getTableCellsFromSelection(selection);
+    const referenceCell = cells[isBefore ? 0 : cells.length - 1];
+    if (!table || !referenceCell) {
+        return;
+    }
+
+    const clipboardCols = columnClipboard.map(cb => cb.get(editor));
+    if (!isBefore) {
+        clipboardCols.reverse();
+    }
+
+    validateColumnsToPaste(clipboardCols, table);
+    const map = new TableMap(table);
+    const cellRange = map.getRangeForCell(referenceCell);
+    if (!cellRange) {
+        return;
+    }
+
+    const colIndex = isBefore ? cellRange.fromX : cellRange.toX;
+    const colWidths = table.getColWidths();
+
+    for (let y = 0; y < map.rowCount; y++) {
+        const relCell = map.getCellAtPosition(colIndex, y);
+        for (const cells of clipboardCols) {
+            const newCell = cells[y];
+            if (isBefore) {
+                relCell.insertBefore(newCell);
+            } else {
+                relCell.insertAfter(newCell);
+            }
+        }
+    }
+
+    const refWidth = colWidths[colIndex];
+    const addedWidths = clipboardCols.map(_ => refWidth);
+    colWidths.splice(isBefore ? colIndex : colIndex + 1, 0, ...addedWidths);
+}
+
+export function $pasteClipboardColumnsBefore(editor: LexicalEditor): void {
+    $pasteClipboardColumns(editor, true);
+}
+
+export function $pasteClipboardColumnsAfter(editor: LexicalEditor): void {
+    $pasteClipboardColumns(editor, false);
 }
\ No newline at end of file
diff --git a/resources/js/wysiwyg/utils/table-map.ts b/resources/js/wysiwyg/utils/table-map.ts
index bc9721d96..607deffe1 100644
--- a/resources/js/wysiwyg/utils/table-map.ts
+++ b/resources/js/wysiwyg/utils/table-map.ts
@@ -2,6 +2,13 @@ import {CustomTableNode} from "../nodes/custom-table";
 import {$isCustomTableCellNode, CustomTableCellNode} from "../nodes/custom-table-cell";
 import {$isTableRowNode} from "@lexical/table";
 
+export type CellRange = {
+    fromX: number;
+    fromY: number;
+    toX: number;
+    toY: number;
+}
+
 export class TableMap {
 
     rowCount: number = 0;
@@ -77,11 +84,11 @@ export class TableMap {
         return this.cells[position];
     }
 
-    public getCellsInRange(fromX: number, fromY: number, toX: number, toY: number): CustomTableCellNode[] {
-        const minX = Math.max(Math.min(fromX, toX), 0);
-        const maxX = Math.min(Math.max(fromX, toX), this.columnCount - 1);
-        const minY = Math.max(Math.min(fromY, toY), 0);
-        const maxY = Math.min(Math.max(fromY, toY), this.rowCount - 1);
+    public getCellsInRange(range: CellRange): CustomTableCellNode[] {
+        const minX = Math.max(Math.min(range.fromX, range.toX), 0);
+        const maxX = Math.min(Math.max(range.fromX, range.toX), this.columnCount - 1);
+        const minY = Math.max(Math.min(range.fromY, range.toY), 0);
+        const maxY = Math.min(Math.max(range.fromY, range.toY), this.rowCount - 1);
 
         const cells = new Set<CustomTableCellNode>();
 
@@ -93,4 +100,37 @@ export class TableMap {
 
         return [...cells.values()];
     }
+
+    public getCellsInColumn(columnIndex: number): CustomTableCellNode[] {
+        return this.getCellsInRange({
+            fromX: columnIndex,
+            toX: columnIndex,
+            fromY: 0,
+            toY: this.rowCount - 1,
+        });
+    }
+
+    public getRangeForCell(cell: CustomTableCellNode): CellRange|null {
+        let range: CellRange|null = null;
+        const cellKey = cell.getKey();
+
+        for (let y = 0; y < this.rowCount; y++) {
+            for (let x = 0; x < this.columnCount; x++) {
+                const index = (y * this.columnCount) + x;
+                const lCell = this.cells[index];
+                if (lCell.getKey() === cellKey) {
+                    if (range === null) {
+                        range = {fromX: x, toX: x, fromY: y, toY: y};
+                    } else {
+                        range.fromX = Math.min(range.fromX, x);
+                        range.toX = Math.max(range.toX, x);
+                        range.fromY = Math.min(range.fromY, y);
+                        range.toY = Math.max(range.toY, y);
+                    }
+                }
+            }
+        }
+
+        return range;
+    }
 }
\ No newline at end of file
diff --git a/resources/js/wysiwyg/utils/tables.ts b/resources/js/wysiwyg/utils/tables.ts
index d0fd17e2c..aa8ec89ba 100644
--- a/resources/js/wysiwyg/utils/tables.ts
+++ b/resources/js/wysiwyg/utils/tables.ts
@@ -168,12 +168,12 @@ export function $mergeTableCellsInSelection(selection: TableSelection): void {
     const fixedToX = selectionShape.toX + ((headCell.getColSpan() || 1) - 1);
     const fixedToY = selectionShape.toY + ((headCell.getRowSpan() || 1) - 1);
 
-    const mergeCells = tableMap.getCellsInRange(
-        selectionShape.fromX,
-        selectionShape.fromY,
-        fixedToX,
-        fixedToY,
-    );
+    const mergeCells = tableMap.getCellsInRange({
+        fromX: selectionShape.fromX,
+        fromY: selectionShape.fromY,
+        toX: fixedToX,
+        toY: fixedToY,
+    });
 
     if (mergeCells.length === 0) {
         return;