diff --git a/cmd/frostfs-cli/internal/client/client.go b/cmd/frostfs-cli/internal/client/client.go
index 215490dbe..a0fa22410 100644
--- a/cmd/frostfs-cli/internal/client/client.go
+++ b/cmd/frostfs-cli/internal/client/client.go
@@ -2,10 +2,13 @@ package internal
 
 import (
 	"bytes"
+	"cmp"
 	"context"
 	"errors"
 	"fmt"
 	"io"
+	"os"
+	"slices"
 	"sort"
 	"strings"
 
@@ -869,3 +872,65 @@ func SyncContainerSettings(ctx context.Context, prm SyncContainerPrm) (*SyncCont
 
 	return new(SyncContainerRes), nil
 }
+
+// PatchObjectPrm groups parameters of PatchObject operation.
+type PatchObjectPrm struct {
+	commonObjectPrm
+	objectAddressPrm
+
+	NewAttributes []objectSDK.Attribute
+
+	ReplaceAttribute bool
+
+	PayloadPatches []PayloadPatch
+}
+
+type PayloadPatch struct {
+	Range objectSDK.Range
+
+	PayloadPath string
+}
+
+type PatchRes struct {
+	OID oid.ID
+}
+
+func Patch(ctx context.Context, prm PatchObjectPrm) (*PatchRes, error) {
+	patchPrm := client.PrmObjectPatch{
+		XHeaders:    prm.xHeaders,
+		BearerToken: prm.bearerToken,
+		Session:     prm.sessionToken,
+		Address:     prm.objAddr,
+	}
+
+	slices.SortFunc(prm.PayloadPatches, func(a, b PayloadPatch) int {
+		return cmp.Compare(a.Range.GetOffset(), b.Range.GetOffset())
+	})
+
+	patcher, err := prm.cli.ObjectPatchInit(ctx, patchPrm)
+	if err != nil {
+		return nil, fmt.Errorf("init payload reading: %w", err)
+	}
+
+	if patcher.PatchAttributes(ctx, prm.NewAttributes, prm.ReplaceAttribute) {
+		for _, pp := range prm.PayloadPatches {
+			payloadFile, err := os.OpenFile(pp.PayloadPath, os.O_RDONLY, os.ModePerm)
+			if err != nil {
+				return nil, err
+			}
+			applied := patcher.PatchPayload(ctx, &pp.Range, payloadFile)
+			_ = payloadFile.Close()
+			if !applied {
+				break
+			}
+		}
+	}
+
+	res, err := patcher.Close(ctx)
+	if err != nil {
+		return nil, err
+	}
+	return &PatchRes{
+		OID: res.ObjectID(),
+	}, nil
+}
diff --git a/cmd/frostfs-cli/modules/object/patch.go b/cmd/frostfs-cli/modules/object/patch.go
new file mode 100644
index 000000000..8f03885ab
--- /dev/null
+++ b/cmd/frostfs-cli/modules/object/patch.go
@@ -0,0 +1,151 @@
+package object
+
+import (
+	"fmt"
+	"strconv"
+	"strings"
+
+	internalclient "git.frostfs.info/TrueCloudLab/frostfs-node/cmd/frostfs-cli/internal/client"
+	"git.frostfs.info/TrueCloudLab/frostfs-node/cmd/frostfs-cli/internal/commonflags"
+	"git.frostfs.info/TrueCloudLab/frostfs-node/cmd/frostfs-cli/internal/key"
+	commonCmd "git.frostfs.info/TrueCloudLab/frostfs-node/cmd/internal/common"
+	cid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/container/id"
+	objectSDK "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object"
+	oid "git.frostfs.info/TrueCloudLab/frostfs-sdk-go/object/id"
+	"github.com/spf13/cobra"
+)
+
+const (
+	newAttrsFlagName     = "new-attrs"
+	replaceAttrsFlagName = "replace-attrs"
+	rangeFlagName        = "range"
+	payloadFlagName      = "payload"
+)
+
+var objectPatchCmd = &cobra.Command{
+	Use:   "patch",
+	Run:   patch,
+	Short: "Patch FrostFS object",
+	Long:  "Patch FrostFS object. Each range passed to the command requires to pass a corresponding patch payload.",
+	Example: `
+frostfs-cli -c config.yml -r 127.0.0.1:8080 object patch --cid <CID> --oid <OID> --new-attrs 'key1=val1,key2=val2' --replace-attrs
+frostfs-cli -c config.yml -r 127.0.0.1:8080 object patch --cid <CID> --oid <OID> --range offX:lnX --payload /path/to/payloadX --range offY:lnY --payload /path/to/payloadY
+frostfs-cli -c config.yml -r 127.0.0.1:8080 object patch --cid <CID> --oid <OID> --new-attrs 'key1=val1,key2=val2' --replace-attrs --range offX:lnX --payload /path/to/payload
+`,
+}
+
+func initObjectPatchCmd() {
+	commonflags.Init(objectPatchCmd)
+	initFlagSession(objectPatchCmd, "PATCH")
+
+	flags := objectPatchCmd.Flags()
+
+	flags.String(commonflags.CIDFlag, "", commonflags.CIDFlagUsage)
+	_ = objectRangeCmd.MarkFlagRequired(commonflags.CIDFlag)
+
+	flags.String(commonflags.OIDFlag, "", commonflags.OIDFlagUsage)
+	_ = objectRangeCmd.MarkFlagRequired(commonflags.OIDFlag)
+
+	flags.String(newAttrsFlagName, "", "New object attributes in form of Key1=Value1,Key2=Value2")
+	flags.Bool(replaceAttrsFlagName, false, "Replace object attributes by new ones.")
+	flags.StringSlice(rangeFlagName, []string{}, "Range to which patch payload is applied. Format: offset:length")
+	flags.StringSlice(payloadFlagName, []string{}, "Path to file with patch payload.")
+}
+
+func patch(cmd *cobra.Command, _ []string) {
+	var cnr cid.ID
+	var obj oid.ID
+
+	objAddr := readObjectAddress(cmd, &cnr, &obj)
+
+	ranges, err := getRangeSlice(cmd)
+	commonCmd.ExitOnErr(cmd, "", err)
+
+	payloads := patchPayloadPaths(cmd)
+
+	if len(ranges) != len(payloads) {
+		commonCmd.ExitOnErr(cmd, "", fmt.Errorf("the number of ranges and payloads are not equal: ranges = %d, payloads = %d", len(ranges), len(payloads)))
+	}
+
+	newAttrs, err := parseNewObjectAttrs(cmd)
+	commonCmd.ExitOnErr(cmd, "can't parse new object attributes: %w", err)
+	replaceAttrs, _ := cmd.Flags().GetBool(replaceAttrsFlagName)
+
+	pk := key.GetOrGenerate(cmd)
+
+	cli := internalclient.GetSDKClientByFlag(cmd, pk, commonflags.RPC)
+
+	var prm internalclient.PatchObjectPrm
+	prm.SetClient(cli)
+	Prepare(cmd, &prm)
+	ReadOrOpenSession(cmd, &prm, pk, cnr, nil)
+
+	prm.SetAddress(objAddr)
+	prm.NewAttributes = newAttrs
+	prm.ReplaceAttribute = replaceAttrs
+
+	for i := range ranges {
+		prm.PayloadPatches = append(prm.PayloadPatches, internalclient.PayloadPatch{
+			Range:       ranges[i],
+			PayloadPath: payloads[i],
+		})
+	}
+
+	res, err := internalclient.Patch(cmd.Context(), prm)
+	if err != nil {
+		commonCmd.ExitOnErr(cmd, "can't patch the object: %w", err)
+	}
+	cmd.Println("Patched object ID: ", res.OID.EncodeToString())
+}
+
+func parseNewObjectAttrs(cmd *cobra.Command) ([]objectSDK.Attribute, error) {
+	var rawAttrs []string
+
+	raw := cmd.Flag(newAttrsFlagName).Value.String()
+	if len(raw) != 0 {
+		rawAttrs = strings.Split(raw, ",")
+	}
+
+	attrs := make([]objectSDK.Attribute, len(rawAttrs), len(rawAttrs)+2) // name + timestamp attributes
+	for i := range rawAttrs {
+		k, v, found := strings.Cut(rawAttrs[i], "=")
+		if !found {
+			return nil, fmt.Errorf("invalid attribute format: %s", rawAttrs[i])
+		}
+		attrs[i].SetKey(k)
+		attrs[i].SetValue(v)
+	}
+	return attrs, nil
+}
+
+func getRangeSlice(cmd *cobra.Command) ([]objectSDK.Range, error) {
+	v, _ := cmd.Flags().GetStringSlice(rangeFlagName)
+	if len(v) == 0 {
+		return []objectSDK.Range{}, nil
+	}
+	rs := make([]objectSDK.Range, len(v))
+	for i := range v {
+		before, after, found := strings.Cut(v[i], rangeSep)
+		if !found {
+			return nil, fmt.Errorf("invalid range specifier: %s", v[i])
+		}
+
+		offset, err := strconv.ParseUint(before, 10, 64)
+		if err != nil {
+			return nil, fmt.Errorf("invalid '%s' range offset specifier: %w", v[i], err)
+		}
+		length, err := strconv.ParseUint(after, 10, 64)
+		if err != nil {
+			return nil, fmt.Errorf("invalid '%s' range length specifier: %w", v[i], err)
+		}
+
+		rs[i].SetOffset(offset)
+		rs[i].SetLength(length)
+	}
+	return rs, nil
+}
+
+func patchPayloadPaths(cmd *cobra.Command) []string {
+	v, _ := cmd.Flags().GetStringSlice(payloadFlagName)
+	return v
+}
diff --git a/cmd/frostfs-cli/modules/object/root.go b/cmd/frostfs-cli/modules/object/root.go
index 7d8008b10..b808a509e 100644
--- a/cmd/frostfs-cli/modules/object/root.go
+++ b/cmd/frostfs-cli/modules/object/root.go
@@ -29,6 +29,7 @@ func init() {
 		objectRangeCmd,
 		objectLockCmd,
 		objectNodesCmd,
+		objectPatchCmd,
 	}
 
 	Cmd.AddCommand(objectChildCommands...)
@@ -39,6 +40,7 @@ func init() {
 	}
 
 	initObjectPutCmd()
+	initObjectPatchCmd()
 	initObjectDeleteCmd()
 	initObjectGetCmd()
 	initObjectSearchCmd()
diff --git a/cmd/frostfs-cli/modules/object/util.go b/cmd/frostfs-cli/modules/object/util.go
index 381c790e9..96b80fe1b 100644
--- a/cmd/frostfs-cli/modules/object/util.go
+++ b/cmd/frostfs-cli/modules/object/util.go
@@ -306,6 +306,8 @@ func finalizeSession(cmd *cobra.Command, dst SessionPrm, tok *session.Object, ke
 	case *internal.PutObjectPrm:
 		common.PrintVerbose(cmd, "Binding session to object PUT...")
 		tok.ForVerb(session.VerbObjectPut)
+	case *internal.PatchObjectPrm:
+		tok.ForVerb(session.VerbObjectPatch)
 	case *internal.DeleteObjectPrm:
 		common.PrintVerbose(cmd, "Binding session to object DELETE...")
 		tok.ForVerb(session.VerbObjectDelete)