diff --git a/include/ovn/actions.h b/include/ovn/actions.h
index d7ee84dac..5814a34aa 100644
--- a/include/ovn/actions.h
+++ b/include/ovn/actions.h
@@ -74,6 +74,7 @@ struct ovn_extend_table;
     OVNACT(CT_LB_MARK,        ovnact_ct_lb)           \
     OVNACT(SELECT,            ovnact_select)          \
     OVNACT(CT_CLEAR,          ovnact_null)            \
+    OVNACT(CT_COMMIT_NAT,     ovnact_ct_nat)          \
     OVNACT(CLONE,             ovnact_nest)            \
     OVNACT(ARP,               ovnact_nest)            \
     OVNACT(ICMP4,             ovnact_nest)            \
@@ -275,6 +276,8 @@ struct ovnact_ct_nat {
        uint16_t port_hi;
     } port_range;
 
+    bool commit;                /* Explicit commit action. */
+
     uint8_t ltable;             /* Logical table ID of next table. */
 };
 
diff --git a/lib/actions.c b/lib/actions.c
index adbb42db4..91bbabc0e 100644
--- a/lib/actions.c
+++ b/lib/actions.c
@@ -920,6 +920,7 @@ parse_ct_nat(struct action_context *ctx, const char *name,
         return;
     }
     cn->ltable = ctx->pp->cur_ltable + 1;
+    cn->commit = false;
 
     if (lexer_match(ctx->lexer, LEX_T_LPAREN)) {
         if (ctx->lexer->token.type != LEX_T_INTEGER
@@ -929,9 +930,11 @@ parse_ct_nat(struct action_context *ctx, const char *name,
             return;
         }
         if (ctx->lexer->token.format == LEX_F_IPV4) {
+            cn->commit = true;
             cn->family = AF_INET;
             cn->ipv4 = ctx->lexer->token.value.ipv4;
         } else if (ctx->lexer->token.format == LEX_F_IPV6) {
+            cn->commit = true;
             cn->family = AF_INET6;
             cn->ipv6 = ctx->lexer->token.value.ipv6;
         }
@@ -1004,6 +1007,24 @@ parse_CT_SNAT_IN_CZONE(struct action_context *ctx)
                  ovnact_put_CT_SNAT_IN_CZONE(ctx->ovnacts));
 }
 
+static void
+parse_CT_COMMIT_NAT(struct action_context *ctx)
+{
+    add_prerequisite(ctx, "ip");
+
+    if (ctx->pp->cur_ltable >= ctx->pp->n_tables) {
+        lexer_error(ctx->lexer,
+                    "\"ct_commit_related\" action not allowed in last table.");
+        return;
+    }
+
+    struct ovnact_ct_nat *cn = ovnact_put_CT_COMMIT_NAT(ctx->ovnacts);
+    cn->commit = true;
+    cn->ltable = ctx->pp->cur_ltable + 1;
+    cn->family = AF_UNSPEC;
+    cn->port_range.exists = false;
+}
+
 static void
 format_ct_nat(const struct ovnact_ct_nat *cn, const char *name, struct ds *s)
 {
@@ -1053,6 +1074,12 @@ format_CT_SNAT_IN_CZONE(const struct ovnact_ct_nat *cn, struct ds *s)
     format_ct_nat(cn, "ct_snat_in_czone", s);
 }
 
+static void
+format_CT_COMMIT_NAT(const struct ovnact_ct_nat *cn OVS_UNUSED, struct ds *s)
+{
+    ds_put_cstr(s, "ct_commit_nat;");
+}
+
 static void
 encode_ct_nat(const struct ovnact_ct_nat *cn,
               const struct ovnact_encode_params *ep,
@@ -1104,7 +1131,7 @@ encode_ct_nat(const struct ovnact_ct_nat *cn,
 
     ofpacts->header = ofpbuf_push_uninit(ofpacts, nat_offset);
     ct = ofpacts->header;
-    if (cn->family == AF_INET || cn->family == AF_INET6) {
+    if (cn->commit) {
         ct->flags |= NX_CT_F_COMMIT;
     }
     ofpact_finish(ofpacts, &ct->ofpact);
@@ -1143,6 +1170,17 @@ encode_CT_SNAT_IN_CZONE(const struct ovnact_ct_nat *cn,
     encode_ct_nat(cn, ep, true, ep->common_nat_ct_zone, ofpacts);
 }
 
+static void
+encode_CT_COMMIT_NAT(const struct ovnact_ct_nat *cn,
+                         const struct ovnact_encode_params *ep,
+                         struct ofpbuf *ofpacts)
+{
+    enum mf_field_id zone = ep->is_switch
+                            ? MFF_LOG_CT_ZONE
+                            : MFF_LOG_DNAT_ZONE;
+    encode_ct_nat(cn, ep, false, zone, ofpacts);
+}
+
 static void
 ovnact_ct_nat_free(struct ovnact_ct_nat *ct_nat OVS_UNUSED)
 {
@@ -4732,6 +4770,8 @@ parse_action(struct action_context *ctx)
         parse_ct_lb_action(ctx, true);
     } else if (lexer_match_id(ctx->lexer, "ct_clear")) {
         ovnact_put_CT_CLEAR(ctx->ovnacts);
+    } else if (lexer_match_id(ctx->lexer, "ct_commit_nat")) {
+        parse_CT_COMMIT_NAT(ctx);
     } else if (lexer_match_id(ctx->lexer, "clone")) {
         parse_CLONE(ctx);
     } else if (lexer_match_id(ctx->lexer, "arp")) {
diff --git a/ovn-sb.xml b/ovn-sb.xml
index 315d60853..75becdaf3 100644
--- a/ovn-sb.xml
+++ b/ovn-sb.xml
@@ -1467,6 +1467,18 @@
           Clears connection tracking state.
         </dd>
 
+        <dt><code>ct_commit_nat;</code></dt>
+        <dd>
+          <p>
+            Applies NAT and commits the connection to the CT. Automatically
+            moves on to the next table, as if followed by
+            <code>next</code>.
+            This is very useful for connections that are in related state for
+            already existing connections and allows the NAT to be applied
+            to them as well.
+          </p>
+        </dd>
+
         <dt><code>clone { <var>action</var>; </code>...<code> };</code></dt>
         <dd>
           Makes a copy of the packet being processed and executes each
diff --git a/tests/ovn.at b/tests/ovn.at
index f8b8db4df..65edb7812 100644
--- a/tests/ovn.at
+++ b/tests/ovn.at
@@ -1418,6 +1418,11 @@ ct_snat_in_czone(192.168.1.2, 1000-100);
 ct_clear;
     encodes as ct_clear
 
+# ct_commit_nat
+ct_commit_nat;
+    encodes as ct(commit,table=19,zone=NXM_NX_REG13[0..15],nat)
+    has prereqs ip
+
 # clone
 clone { ip4.dst = 255.255.255.255; output; }; next;
     encodes as clone(set_field:255.255.255.255->ip_dst,resubmit(,64)),resubmit(,19)
diff --git a/utilities/ovn-trace.c b/utilities/ovn-trace.c
index 6fa5137d9..200b7507b 100644
--- a/utilities/ovn-trace.c
+++ b/utilities/ovn-trace.c
@@ -2438,6 +2438,35 @@ execute_ct_nat(const struct ovnact_ct_nat *ct_nat,
      * flow, not ct_flow. */
 }
 
+static void
+execute_ct_commit_nat(const struct ovnact_ct_nat *ct_nat,
+                      const struct ovntrace_datapath *dp, struct flow *uflow,
+                      enum ovnact_pipeline pipeline, struct ovs_list *super)
+{
+    struct flow ct_flow = *uflow;
+    struct ds s = DS_EMPTY_INITIALIZER;
+
+    ds_put_cstr(&s, "ct_commit_nat /* assuming no"
+                    " un-nat entry, so no change */");
+
+    /* ct(nat) implies ct(). */
+    if (!(ct_flow.ct_state & CS_TRACKED)) {
+        ct_flow.ct_state |= next_ct_state(&s);
+    }
+
+    struct ovntrace_node *node = ovntrace_node_append(
+        super, OVNTRACE_NODE_TRANSFORMATION, "%s", ds_cstr(&s));
+    ds_destroy(&s);
+
+    /* Trace the actions in the next table. */
+    trace__(dp, &ct_flow, ct_nat->ltable, pipeline, &node->subs);
+
+    /* Upon return, we will trace the actions following the ct action in the
+     * original table.  The pipeline forked, so we're using the original
+     * flow, not ct_flow. */
+}
+
+
 static void
 execute_ct_lb(const struct ovnact_ct_lb *ct_lb,
               const struct ovntrace_datapath *dp, struct flow *uflow,
@@ -3095,6 +3124,11 @@ trace_actions(const struct ovnact *ovnacts, size_t ovnacts_len,
             flow_clear_conntrack(uflow);
             break;
 
+        case OVNACT_CT_COMMIT_NAT:
+            execute_ct_commit_nat(ovnact_get_CT_COMMIT_NAT(a), dp, uflow,
+                                  pipeline, super);
+            break;
+
         case OVNACT_CLONE:
             execute_clone(ovnact_get_CLONE(a), dp, uflow, table_id, pipeline,
                           super);
